﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using Silvermoon.OpenGL.Native;
using Silvermoon.Controls;

namespace Silvermoon.OpenGL
{
    public class TextSprite : IDisposable
    {
        #region fields

        const int ellipsisCharacters = 3;

        private Size textureSize;
        private int visibleChars;
        private int skew;
        private bool invalidated = true;

        /// <summary>
        /// If rebuild is true, the font characters are recalculated again, otherwise only the characters for the display are recalculated.
        /// </summary>
        private bool rebuild = true;

        private Size size;
        private Size textSize;
        private float fontScale = 1f;
        private FontSprite font;
        private string text = string.Empty;
        private LineMode lineMode = LineMode.SingleLine;
        private Alignment alignment = Alignment.Near;
        private Alignment lineAlignment = Alignment.Center;
        private int charSpace;

        /// <summary>
        /// Contains edges in the texture from FontSprite for each character.
        /// </summary>
        protected VectorQuad2i[] textureMap;

        /// <summary>
        /// Contains the edges on the display for each character.
        /// </summary>
        protected VectorQuads[] vertexMap;

        /// <summary>
        /// Note that the format of the indexes is always the same 0,1,2,2,1,3, ... and thus can be shared with other TextSprites:
        /// </summary>
        protected static short[] indexes;

        #endregion
        #region ctors

        public TextSprite()
            : base()
        {
        }

        public virtual void Dispose()
        {
        }

        #endregion
        #region props

        /// <summary>
        /// Gets or sets the skewness of the character. The bigger the value, the more italic the character appears.
        /// </summary>
        public int Skew
        {
            get { return skew; }
            set
            {
                if (skew != value)
                {
                    skew = value;
                    Invalidate();
                }
            }
        }

        /// <summary>
        /// Gets the length of the text.
        /// </summary>
        public int Length
        {
            get { return Text.Length; }
        }

        /// <summary>
        /// Gets the number of characters to render.
        /// </summary>
        public int VisibleCharacters
        {
            get { return visibleChars; }
        }

        public bool IsInvalidated { get { return invalidated; } }

        /// <summary>
        /// Gets or sets the size which is available for the text.
        /// </summary>
        public Size Size
        {
            get { return size; }
            set
            {
                if (size != value)
                {
                    size = value;
                    Invalidate();
                }
            }
        }

        /// <summary>
        /// Gets the size that the text consumes.
        /// </summary>
        public Size TextSize
        {
            get
            {
                PreLoad();
                return textSize;
            }
        }

        public Color Color { get; set; }

        /// <summary>
        /// Gets or sets the font for the text.
        /// </summary>
        public FontSprite Font
        {
            get { return font ?? FontSprite.Regular; }
            set
            {
                if (font != value)
                {
                    rebuild = true;
                    font = value;
                    Invalidate();
                }
            }
        }

        /// <summary>
        /// Gets or sets the text to render.
        /// </summary>
        public string Text
        {
            get
            {
                return text;
            }
            set
            {
                if (value == null) value = string.Empty;
                if (text != value)
                {
                    text = value;
                    OnTextChanged();
                }
            }
        }

        /// <summary>
        /// Gets or sets the number of pixels between each character.
        /// </summary>
        public int CharSpace
        {
            get
            {
                return charSpace;
            }
            set
            {
                if (charSpace != value)
                {
                    charSpace = value;
                    Invalidate();
                }
            }
        }

        public bool WordWrap
        {
            get
            {
                return lineMode == LineMode.WordWrap;
            }
        }

        public bool SingleLine
        {
            get
            {
                return lineMode == LineMode.SingleLine;
            }
        }

        public bool MultiLine
        {
            get
            {
                return lineMode == LineMode.MultiLine;
            }
        }

        public bool EndEllipsis
        {
            get
            {
                return lineMode == LineMode.EndEllipsis;
            }
        }

        public Alignment Alignment
        {
            get
            {
                return alignment;
            }
            set
            {
                if (alignment != value)
                {
                    alignment = value;
                    Invalidate();
                }
            }
        }

        public Alignment LineAlignment
        {
            get
            {
                return lineAlignment;
            }
            set
            {
                if (lineAlignment != value)
                {
                    lineAlignment = value;
                    Invalidate();
                }
            }
        }

        /// <summary>
        /// Gets or sets how to handle multiple lines.
        /// </summary>
        public LineMode LineMode
        {
            get
            {
                return lineMode;
            }
            set
            {
                if (value == LineMode.EndEllipsis) value = LineMode.EndEllipsis | LineMode.SingleLine;
                if (lineMode != value)
                {
                    lineMode = value;
                    Invalidate();
                }
            }
        }

        /// <summary>
        /// Gets or sets the factor how text is scaled. If FontScale>1f, text gets larger, otherwise smaller.
        /// This enables TextSprite to use only one (large) TextFont to render text in different sizes instead of using different TextFont for each size with different underlying textures and
        /// thus saving graphic resources.
        /// </summary>
        public float FontScale
        {
            get
            {
                return fontScale;
            }
            set
            {
                if (fontScale != value)
                {
                    //   rebuild = true;
                    fontScale = value;
                    Invalidate();
                }
            }
        }

        #endregion
        #region methods

        public void Invalidate()
        {
            invalidated = true;
        }

        public void PreLoad()
        {
            if (invalidated)
            {
                LayoutCharacters();
                invalidated = false;
            }
        }

        protected virtual void LayoutCharacters()
        {
            visibleChars = 0;
            Size size = GetScaledSize();

            bool measure = (lineMode & LineMode.EndEllipsis) != 0;
            int clipWidth = measure ? size.Width : int.MaxValue;
            int clipHeight = measure ? size.Height : int.MaxValue;

            textureSize = Font.TextureSize;

            int n = text.Length;
            if (rebuild == false) rebuild = textureMap == null || textureMap.Length != n || visibleChars != n;

            if (indexes == null || indexes.Length / 6 < (n + ellipsisCharacters)) BuildIndexes(n + ellipsisCharacters);
            if (rebuild)
            {
                textureMap = new VectorQuad2i[n];
                vertexMap = new VectorQuads[n];
            }

            FontSprite font = Font;
            int h = font.CharHeight;
            int y = 0;
            int wMax = 0;

            int pos = 0;
            bool multiLine = (lineMode & LineMode.MultiLine) != 0;
            while (pos < n)
            {
                int end = multiLine ? GetLineEnd(pos, clipWidth) : n;
                if (end == pos) end++;
                int x = alignment != Alignment.Near ? GetStartXFromAlignment(size.Width, pos, end) : 0;
                for (int i = pos; i < end; i++)
                {
                    char c = text[i];
                    var cd = font.GetCharData(c);
                    vertexMap[i].SetChar(x, y, cd.Width, h, skew);

                    if (rebuild)
                    {
                        SetTexture(i, cd, h);
                    }
                    if (!(i == end - 1 && IsSpace(c)))
                    {
                        x += cd.Width + charSpace;
                    }
                    if (!multiLine)
                    {
                        if ((x - charSpace) <= size.Width) visibleChars = i + 1;
                    }
                }
                wMax = Math.Max(x, wMax);
                pos = end;
                if (multiLine)
                {
                    if ((y + h) <= clipHeight) visibleChars = pos;
                    y += h;
                }
            }
            if (multiLine && pos > 0 && text[pos - 1] == '\n') y += h;
            if (lineMode == LineMode.EndEllipsis && wMax > clipWidth)
            {
                wMax = AddEllipsis(visibleChars, clipWidth, h);
            }

            wMax += 1;
            textSize = Unscale(new Size(wMax, Math.Max(y + 1, h)));
            rebuild = false;
        }

        private bool IsSpace(char c)
        {
            return " \n\r\t".IndexOf(c) >= 0;
        }

        private int AddEllipsis(int pos, int w, int h)
        {
            var cd = Font.GetCharData('.');
            int ew = cd.Width * ellipsisCharacters;
            if (ew > 0)
            {
                pos--;
                while (pos > 0 && (vertexMap[pos].TopLeft.X + ew) > w) pos--;
                if (pos < 0) return 0;
                var map = vertexMap[pos].TopLeft;
                int x = map.X;
                int y = map.Y;

                int n = text.Length;
                for (int i = pos; i < pos + ellipsisCharacters; i++)
                {
                    if (i >= n) break;
                    vertexMap[i].SetChar(x, y, cd.Width, h, skew);
                    SetTexture(i, cd, h);
                    x += cd.Width;
                }

                visibleChars = pos + ellipsisCharacters;
            }
            return w;
        }

        private Size Unscale(Size size)
        {
            if (fontScale == 1f) return size;
            return new Size((int)(size.Width * fontScale + 0.99f), (int)(size.Height * fontScale + 0.99f));
        }

        private void BuildIndexes(int n)
        {
            n = GetIndexBuffer(n);

            indexes = new short[n * 6];
            for (int i = 0; i < n; i++) AddIndex(i);
        }

        private static int GetIndexBuffer(int n)
        {
            if (n < 128) n = 128;
            else if (n < 512) n = 512;
            else if (n < 2048) n = 2048;
            else if (n < 4096) n = 4096;
            return n;
        }

        private void SetTexture(int i, FontSprite.CharData cd, int h)
        {
            int left = cd.X;
            int top = cd.Y;
            int right = cd.Right;
            int bottom = top + h;

            int dx = textureSize.Width;
            int dy = textureSize.Height;

            left = (left << 16) / dx;
            top = (top << 16) / dy;
            right = (right << 16) / dx;
            bottom = (bottom << 16) / dy;
            textureMap[i].Set(left, top, right, bottom);


        }

        private int GetStartXFromAlignment(int w, int pos, int end)
        {
            switch (alignment)
            {
                case Alignment.Near: return 0;
                case Alignment.Far: return w - GetWidth(pos, end, w);
                case Alignment.Center: return (w - GetWidth(pos, end, w)) / 2;
                default: throw new NotSupportedException();
            }
        }

        private unsafe void AddIndex(int index)
        {
            int stripIndex = (int)(index * 4);
            int ixPos = index * 6;
            fixed (short* ix = &indexes[ixPos])
            {
                *(ix + 0) = (short)stripIndex;
                *(ix + 1) = (short)(stripIndex + 1);
                *(ix + 2) = (short)(stripIndex + 2);
                *(ix + 3) = (short)(stripIndex + 2);
                *(ix + 4) = (short)(stripIndex + 1);
                *(ix + 5) = (short)(stripIndex + 3);
            }
        }

        private Size GetScaledSize()
        {
            if (fontScale == 1f) return size;
            return new Size((int)(size.Width / fontScale + 0.0f), (int)(size.Height / fontScale + 0.0f));
        }

        private int GetWidth(int pos, int max, int wMax)
        {
            int w = 0;
            for (int i = pos; i < max; i++)
            {
                var cd = Font.GetCharData(text[i]);
                w += cd.Width + charSpace;
            }
            if (lineMode == LineMode.EndEllipsis && w > wMax) w = wMax;
            return w;
        }

        private int GetLineEnd(int index, int width)
        {
            bool wordWrap = WordWrap;
            int lastWordIndex = index;
            int n = text.Length;
            for (int i = index; i < n; i++)
            {
                char c = text[i];

                if (IsReturn(c)) return i + 1;
                if (wordWrap && IsCharSeparator(c)) lastWordIndex = i;
                var cd = Font.GetCharData(c);
                if ((width - cd.Width) < 0)
                {
                    return lastWordIndex == index ? i : lastWordIndex + 1;
                }
                width -= cd.Width + charSpace;
            }
            return text.Length;
        }

        private bool IsReturn(char c)
        {
            return c == '\n';
        }

        private bool IsCharSeparator(char c)
        {
            return "\t ,:;-+/\\~*!?=<>@".IndexOf(c) >= 0;
        }

        /// <summary>
        /// Gets the bounds for a given character.
        /// </summary>
        /// <param name="index">Index of character for which to get the rectangle.</param>
        /// <returns>Rectangle with the bounds of the character.</returns>
        public Rectangle GetCharRect(int index)
        {
            PreLoad();
            if (vertexMap == null || vertexMap.Length <= index) return Rectangle.Empty;
            return vertexMap[index].GetRect();
        }

        private int GetLineCount()
        {
            if (text.Length == 0) return 1;
            PreLoad();
            int n = vertexMap[vertexMap.Length - 1].BottomRight.Y / Font.CharHeight;
            return Math.Min(1, n);
        }

        /// <summary>
        /// Renders the text.
        /// </summary>
        public void Render()
        {
            Render(0, visibleChars);
        }

        /// <summary>
        /// Renders parts of the text.
        /// </summary>
        /// <param name="offset">offset where to start.</param>
        /// <param name="len">number of characters to render.</param>
        public unsafe virtual void Render(int offset, int len)
        {
            PreLoad();
            if ((len + offset > Length)) len = Length - offset;
            if (len <= 0 || Color.A < 2) return;

            int y = GetYOffset();
            if (fontScale != 1f)
            {
                gl.PushMatrix();
                if (y != 0) gl.Translatef(0, y, 0);
                gl.Scalef(fontScale, fontScale, 1f);
            }
            else
            {
                if (y != 0) gl.Translatef(0, y, 0);
            }

            PreRender(offset, len);


            Color color = Color;
            gl.Color4x(color.R << 8, color.G << 8, color.B << 8, color.A << 8);
            gl.CheckError();

            gl.BindTexture(TextureTarget.Texture2D, Font.Texture.Name);
            gl.CheckError();

            fixed (void* v = &textureMap[offset])
            {
                gl.TexCoordPointer(2, glValueType.Fixed, 0, v);
            }
            fixed (void* v = &vertexMap[offset])
            {
                gl.VertexPointer(2, glValueType.Short, 0, v);
            }

            gl.DrawElements(PrimitiveType.Triangles, 6 * len, glValueType.UnsignedShort, indexes);
            gl.CheckError();
            if (fontScale != 1f)
            {
                gl.PopMatrix();
            }
            else
            {
                if (y != 0) gl.Translatex(0, -y, 0);
            }
        }

        protected virtual void PreRender(int offset, int len)
        {
        }

        protected int GetYOffset()
        {
            switch (lineAlignment)
            {
                case Alignment.Near: return 0;
                case Alignment.Far: return size.Height - textSize.Height;
                case Alignment.Center: return (size.Height - textSize.Height) / 2;
                default: throw new NotSupportedException();
            }
        }

        /// <summary>
        /// Gets the index of the character which is under a specified point.
        /// </summary>
        /// <param name="point">Point to determine. </param>
        /// <returns>Index of the character, otherwise -1.</returns>
        public int GetIndexFromPoint(Point point)
        {
            int index = SearchHelper.BinarySearchLeEq(point, vertexMap, Length);
            if (index >= 0 && index < Length)
            {
                Rectangle rect = vertexMap[index].GetRect();
                if (!rect.Contains(point)) index++;
                return index;
            }
            return -1;
        }

        protected virtual void OnTextChanged()
        {
            rebuild = true;
            Invalidate();
        }


        #endregion
    }
}
